package com.fjwt.gz.coupon.hkpay.hkpayutil;

import cn.hutool.json.JSONUtil;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.module.SimpleModule;
import lombok.Data;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;


public class HkApiClient {
    private static final Logger hkPayLogger = LoggerFactory.getLogger("hkPay");

    private final static Charset charset = StandardCharsets.UTF_8;
    private final static ObjectMapper mapper = new ObjectMapper()
            .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
            .disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)
            .enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE)
            .enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS)
            .registerModule(new SimpleModule() {{
                addSerializer(LocalDateTime.class, new JsonSerializer<LocalDateTime>() {
                    public void serialize(LocalDateTime value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
                        gen.writeString(value.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
                    }
                });
                addDeserializer(LocalDateTime.class, new JsonDeserializer<LocalDateTime>() {
                    public LocalDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
                        return LocalDateTime.parse(p.getValueAsString(), DateTimeFormatter.ISO_LOCAL_DATE_TIME);
                    }
                });
            }});
    private final String address;
    private final String clientKey;
    private final String securityKey;

    public HkApiClient(String clientKey, String securityKey, String address) {
        this.clientKey = clientKey;
        this.securityKey = securityKey;
        this.address = address;
    }

    public <T, R> ApiResponse<R> request(String service, String version, String name, T data, Class<R> returnClass) throws IOException {
        hkPayLogger.info("海科接口调用请求参数：{}", JSONUtil.toJsonStr(data));

        ApiRequest<T> request = new ApiRequest<>();
        request.setName(name);
        request.setClientKey(clientKey);
        request.setTimestamp(LocalDateTime.now());
        request.setData(data);

        hkPayLogger.info("海科接口调用待加密参数：{}", JSONUtil.toJsonStr(request));

        Map<String, String> form = encrypt(request);

        hkPayLogger.info("海科接口调用加密后参数：{}", JSONUtil.toJsonStr(form));

        String url = address + service + "/" + version;
        return post(url, form, returnClass);
    }

    protected <T> Map<String, String> encrypt(ApiRequest<T> request) {
        try {
            Map<String, String> form = new HashMap<>();
            String req = mapper.writeValueAsString(request);
            String sign = hmacSha256(securityKey, req);
            form.put("req", req);
            form.put("sign", sign);
            return form;
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }
    }

    protected String decrypt(String body, String sign) {
        if (!hmacSha256(securityKey, body).equals(sign)) {
            throw new RuntimeException("签名不正确");
        }
        return body;
    }

    private String hmacSha256(String key, String data) {
        byte[] sign = hmacSha256(key.getBytes(StandardCharsets.UTF_8), data.getBytes(StandardCharsets.UTF_8));
        return Base64.getEncoder().encodeToString(sign);
    }

    private byte[] hmacSha256(byte[] key, byte[] data) {
        try {
            SecretKey secretKey = new SecretKeySpec(key, "HmacSHA256");
            Mac mac = Mac.getInstance("HmacSHA256");
            mac.init(secretKey);
            return mac.doFinal(data);
        } catch (Exception e) {
            throw new IllegalStateException("签名出现严重问题", e);
        }
    }

    private <R> ApiResponse<R> post(String u, Map<String, String> form, Class<R> returnClass) throws IOException {
        URL url = new URL(u);
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
        conn.setRequestMethod("POST");
        conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
        conn.setUseCaches(false);
        conn.setDoOutput(true);

        try (OutputStreamWriter wr = new OutputStreamWriter(conn.getOutputStream(), charset)) {
            StringBuilder sb = new StringBuilder();
            for (Map.Entry<String, String> entry : form.entrySet()) {
                if (sb.length() > 0) {
                    sb.append('&');
                }
                sb.append(entry.getKey()).append('=').append(URLEncoder.encode(entry.getValue(), "UTF-8"));
            }
            wr.write(sb.toString());
            wr.flush();

            try (InputStream input = conn.getInputStream()) {
                String sign = conn.getHeaderField("Api-Sign");
                String body = decrypt(IOUtils.toString(input, charset), sign);
                JavaType type = mapper.getTypeFactory().constructParametricType(ApiResponse.class, returnClass);
                return mapper.readValue(body, type);
            }
        }
    }

    @Data
    public static class ApiRequest<T> {
        private String name;
        private String clientKey;
        private LocalDateTime timestamp;
        private T data;
    }

    @Data
    public static class ApiResponse<T> {
        private String name;
        private String code;
        private String message;
        private T data;
    }
}
